// Stephen Toub
// stoub@microsoft.com
//
// MidiCodeGenerator.cs
// Class that generates the source code necessary to recreate
// a given MIDI file when executed.

#region Namespaces
using System;
using System.IO;
using System.CodeDom;
using System.CodeDom.Compiler;
#endregion

namespace Toub.Sound.Midi
{
	/// <summary>Generator to render code to generate a given MIDI file.</summary>
	public sealed class MidiCodeGenerator
	{
		#region Construction
		/// <summary>Prevent external instantiation.</summary>
		private MidiCodeGenerator() {}
		#endregion

		#region Code Generation
		#region Main Generation
		/// <summary>Creates code from a MIDI sequence.</summary>
		/// <param name="generator">Generator used to generate the actual code.</param>
		/// <param name="sequence">The sequence to translate.</param>
		/// <param name="midiName">The name of the MIDI sequence to be used as the class name.</param>
		/// <param name="writer">The writer to which the text is written.</param>
		public static void GenerateMIDICode(
			ICodeGenerator generator, MidiSequence sequence, 
			string midiName, TextWriter writer)
		{
			// Create a new namespace
			CodeNamespace ns = new CodeNamespace("AutoGenerated");

			// Create the new class
			CodeTypeDeclaration midiType = new CodeTypeDeclaration(midiName);

			// Add a method to it to create the sequence
			midiType.Members.Add(CreateSequenceMethod(sequence));
			for(int i=0; i<sequence.NumberOfTracks; i++)
			{
				midiType.Members.Add(CreateTrackMethod("CreateTrack" + (i+1).ToString(), sequence[i]));
			}

			// Add the class to the namespace
			ns.Types.Add(midiType);

			// Generate the code
			generator.GenerateCodeFromNamespace(ns, writer, new CodeGeneratorOptions());
		}
		#endregion

		#region Sequence and Track Generation
		/// <summary>Creates the code for the sequence.</summary>
		/// <param name="sequence">The sequence for which we want code.</param>
		/// <returns>The method that creates the sequence.</returns>
		private static CodeMemberMethod CreateSequenceMethod(MidiSequence sequence)
		{
			// Create the method
			CodeMemberMethod method = new CodeMemberMethod();
			method.Attributes = MemberAttributes.Public;
			method.Name = "CreateSequence";
			method.ReturnType = new CodeTypeReference(typeof(MidiSequence));

			// MidiSequence sequence = new MidiSequence(format, division);
			method.Statements.Add(
				new CodeVariableDeclarationStatement(typeof(MidiSequence), "sequence",
				new CodeObjectCreateExpression(typeof(MidiSequence),
				new CodeExpression[]{
										new CodePrimitiveExpression(sequence.Format),
										new CodePrimitiveExpression(sequence.Division)})));

			// sequence.AddTrack(CreateTrack1());
			// sequence.AddTrack(CreateTrack2());
			// ...
			// sequence.AddTrack(CreateTrackN());
			for(int i=0; i<sequence.NumberOfTracks; i++)
			{
				method.Statements.Add(new CodeMethodInvokeExpression(
					new CodeVariableReferenceExpression("sequence"),
					"AddTrack",
					new CodeExpression[]{
											new CodeMethodInvokeExpression(
											new CodeThisReferenceExpression(),
											"CreateTrack" + (i+1).ToString())}));
			}

			// return sequence;
			method.Statements.Add(new CodeMethodReturnStatement(
				new CodeVariableReferenceExpression("sequence")));
			
			// Return the newly created method
			return method;
		}

		/// <summary>Creates a method to create the track.</summary>
		/// <param name="name">The name of the track method.</param>
		/// <param name="track">The track to translate.</param>
		/// <returns>The method that creates the track.</returns>
		private static CodeMemberMethod CreateTrackMethod(string name, MidiTrack track)
		{
			// Create the method
			CodeMemberMethod method = new CodeMemberMethod();
			method.Name = name;
			method.ReturnType = new CodeTypeReference(typeof(MidiTrack));

			// MidiTrack track = new MidiTrack();
			method.Statements.Add(
				new CodeVariableDeclarationStatement(typeof(MidiTrack), "track",
				new CodeObjectCreateExpression(typeof(MidiTrack),
				new CodeExpression[]{})));

			// Add all of the events!
			for(int i=0; i<track.Events.Count; i++)
			{
				CodeObjectCreateExpression createEvent = CreateEvent(track.Events[i]);

				// track.Events.Add(new "event"());
				if (createEvent != null) 
				{
					method.Statements.Add(
						new CodeMethodInvokeExpression(
						new CodePropertyReferenceExpression(
						new CodeVariableReferenceExpression("track"), "Events"),
						"Add", new CodeExpression[]{createEvent}));
				}
			}

			// return track;
			method.Statements.Add(new CodeMethodReturnStatement(
				new CodeVariableReferenceExpression("track")));
			
			// Return the newly created method
			return method;
		}
		#endregion

		#region Midi Event Generation
		/// <summary>Creates an object creation expression for an event.</summary>
		/// <param name="ev">The event to create.</param>
		/// <returns>The object creation expression for the event.</returns>
		private static CodeObjectCreateExpression CreateEvent(MidiEvent ev)
		{
			CodeObjectCreateExpression newEvent = null;

			// Generate correct event object
			if (ev is SystemExclusiveMidiEvent) newEvent = CreateSystemEvent(ev);
			else if (ev is MetaMidiEvent) newEvent = CreateMetaEvent(ev);
			else if (ev is VoiceMidiEvent) newEvent = CreateVoiceEvent(ev);

			// Return the event
			return newEvent;
		}

		/// <summary>Creates an object creation expression for an event.</summary>
		/// <param name="ev">The event to create.</param>
		/// <returns>The object creation expression for the event.</returns>
		private static CodeObjectCreateExpression CreateMetaEvent(MidiEvent ev)
		{
			CodeObjectCreateExpression newEvent = null;
			CodeExpression delta = new CodePrimitiveExpression(ev.DeltaTime);

			// SEQUENCE NUMBER
			if (ev is SequenceNumber)
			{
				SequenceNumber midiEvent = (SequenceNumber)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(SequenceNumber),
					new CodeExpression[]{
											delta,
											new CodePrimitiveExpression(midiEvent.Number)
										});
			}

			// TEXT
			else if (ev is Text)
			{
				Text midiEvent = (Text)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(Text),
					new CodeExpression[]{
											delta,
											new CodePrimitiveExpression(midiEvent.Text)
										});
			}

				// Copyright
			else if (ev is Copyright)
			{
				Copyright midiEvent = (Copyright)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(Copyright),
					new CodeExpression[]{
											delta,
											new CodePrimitiveExpression(midiEvent.Text)
										});
			}

				// SEQUENCE TRACK NAME
			else if (ev is SequenceTrackName)
			{
				SequenceTrackName midiEvent = (SequenceTrackName)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(SequenceTrackName),
					new CodeExpression[]{
											delta,
											new CodePrimitiveExpression(midiEvent.Text)
										});
			}

				// INSTRUMENT
			else if (ev is Instrument)
			{
				Instrument midiEvent = (Instrument)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(Instrument),
					new CodeExpression[]{
											delta,
											new CodePrimitiveExpression(midiEvent.Text)
										});
			}
			
				// Lyric
			else if (ev is Lyric)
			{
				Lyric midiEvent = (Lyric)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(Lyric),
					new CodeExpression[]{
											delta,
											new CodePrimitiveExpression(midiEvent.Text)
										});
			}

				// Marker
			else if (ev is Marker)
			{
				Marker midiEvent = (Marker)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(Marker),
					new CodeExpression[]{
											delta,
											new CodePrimitiveExpression(midiEvent.Text)
										});
			}

				// CuePoint
			else if (ev is CuePoint)
			{
				CuePoint midiEvent = (CuePoint)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(CuePoint),
					new CodeExpression[]{
											delta,
											new CodePrimitiveExpression(midiEvent.Text)
										});
			}
			
				// ProgramName
			else if (ev is ProgramName)
			{
				ProgramName midiEvent = (ProgramName)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(ProgramName),
					new CodeExpression[]{
											delta,
											new CodePrimitiveExpression(midiEvent.Text)
										});
			}

				// ProgramName
			else if (ev is DeviceName)
			{
				DeviceName midiEvent = (DeviceName)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(DeviceName),
					new CodeExpression[]{
											delta,
											new CodePrimitiveExpression(midiEvent.Text)
										});
			}

				// ChannelPrefix
			else if (ev is ChannelPrefix)
			{
				ChannelPrefix midiEvent = (ChannelPrefix)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(ChannelPrefix),
					new CodeExpression[]{
											delta,
											new CodePrimitiveExpression(midiEvent.Prefix)
										});
			}

				// MidiPort
			else if (ev is MidiPort)
			{
				MidiPort midiEvent = (MidiPort)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(MidiPort),
					new CodeExpression[]{
											delta,
											new CodePrimitiveExpression(midiEvent.Port)
										});
			}

				// EndOfTrack
			else if (ev is EndOfTrack)
			{
				EndOfTrack midiEvent = (EndOfTrack)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(EndOfTrack),
					new CodeExpression[]{
											delta
										});
			}

				// Tempo
			else if (ev is Tempo)
			{
				Tempo midiEvent = (Tempo)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(Tempo),
					new CodeExpression[]{
											delta,
											new CodePrimitiveExpression(midiEvent.Value)
										});
			}

				// SMPTEOffset
			else if (ev is SMPTEOffset)
			{
				SMPTEOffset midiEvent = (SMPTEOffset)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(SMPTEOffset),
					new CodeExpression[]{
											delta,
											new CodePrimitiveExpression(midiEvent.Hours),
											new CodePrimitiveExpression(midiEvent.Minutes),
											new CodePrimitiveExpression(midiEvent.Seconds),
											new CodePrimitiveExpression(midiEvent.Frames),
											new CodePrimitiveExpression(midiEvent.FractionalFrames)
										});
			}

				// TimeSignature
			else if (ev is TimeSignature)
			{
				TimeSignature midiEvent = (TimeSignature)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(TimeSignature),
					new CodeExpression[]{
											delta,
											new CodePrimitiveExpression(midiEvent.Numerator),
											new CodePrimitiveExpression(midiEvent.Denominator),
											new CodePrimitiveExpression(midiEvent.MidiClocksPerClick),
											new CodePrimitiveExpression(midiEvent.NumberOfNotated32nds)
										});
			}

				// KeySignature
			else if (ev is KeySignature)
			{
				KeySignature midiEvent = (KeySignature)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(KeySignature),
					new CodeExpression[]{
											delta,
											new CodeCastExpression(typeof(Key), new CodePrimitiveExpression((byte)midiEvent.Key)),
											new CodeCastExpression(typeof(Tonality), new CodePrimitiveExpression((byte)midiEvent.Tonality))
										});
			}

				// Proprietary
			else if (ev is Proprietary)
			{
				Proprietary midiEvent = (Proprietary)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(Proprietary),
					new CodeExpression[]{
											delta,
											CreateDataArray(midiEvent.Data)
										});
			}

				// UnknownMetaMidiEvent
			else if (ev is UnknownMetaMidiEvent)
			{
				UnknownMetaMidiEvent midiEvent = (UnknownMetaMidiEvent)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(UnknownMetaMidiEvent),
					new CodeExpression[]{
											delta,
											new CodePrimitiveExpression(midiEvent.MetaEventID),
											CreateDataArray(midiEvent.Data)
										});
			}
			
			// Return the event
			return newEvent;
		}

		/// <summary>Creates an object creation expression for an event.</summary>
		/// <param name="ev">The event to create.</param>
		/// <returns>The object creation expression for the event.</returns>
		private static CodeObjectCreateExpression CreateSystemEvent(MidiEvent ev)
		{
			CodeObjectCreateExpression newEvent = null;
			CodeExpression delta = new CodePrimitiveExpression(ev.DeltaTime);

			// SYSTEM EXCLUSIVE
			if (ev is SystemExclusiveMidiEvent)
			{
				SystemExclusiveMidiEvent midiEvent = (SystemExclusiveMidiEvent)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(SystemExclusiveMidiEvent),
					new CodeExpression[]{
											delta,
											CreateDataArray(midiEvent.Data)
										});
			}
			
			// Return the event
			return newEvent;
		}
		
		/// <summary>Creates an object creation expression for an event.</summary>
		/// <param name="ev">The event to create.</param>
		/// <returns>The object creation expression for the event.</returns>
		private static CodeObjectCreateExpression CreateVoiceEvent(MidiEvent ev)
		{
			CodeObjectCreateExpression newEvent = null;
			CodeExpression delta = new CodePrimitiveExpression(ev.DeltaTime);

				// NOTE ON
			if (ev is NoteOn)
			{
				NoteOn midiEvent = (NoteOn)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(NoteOn),
					new CodeExpression[]{
											delta, 
											new CodePrimitiveExpression(midiEvent.Channel),
											new CodePrimitiveExpression(MidiEvent.GetNoteName(midiEvent.Note)),
											new CodePrimitiveExpression(midiEvent.Velocity)
										});
			}

				// NOTE OFF
			else if (ev is NoteOff)
			{
				NoteOff midiEvent = (NoteOff)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(NoteOff),
					new CodeExpression[]{
											delta, 
											new CodePrimitiveExpression(midiEvent.Channel),
											new CodePrimitiveExpression(MidiEvent.GetNoteName(midiEvent.Note)),
											new CodePrimitiveExpression(midiEvent.Velocity)
										});
			}

				// AFTERTOUCH
			else if (ev is Aftertouch)
			{
				Aftertouch midiEvent = (Aftertouch)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(Aftertouch),
					new CodeExpression[]{
											delta, 
											new CodePrimitiveExpression(midiEvent.Channel),
											new CodePrimitiveExpression(MidiEvent.GetNoteName(midiEvent.Note)),
											new CodePrimitiveExpression(midiEvent.Pressure)
										});
			}

				// PROGRAM CHANGE
			else if (ev is ProgramChange)
			{
				ProgramChange midiEvent = (ProgramChange)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(ProgramChange),
					new CodeExpression[]{
											delta, 
											new CodePrimitiveExpression(midiEvent.Channel),
											new CodeCastExpression(typeof(GeneralMidiInstruments), new CodePrimitiveExpression(midiEvent.Number))
										});
			}

				// CONTROLLER
			else if (ev is Controller)
			{
				Controller midiEvent = (Controller)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(Controller),
					new CodeExpression[]{
											delta, 
											new CodePrimitiveExpression(midiEvent.Channel),
											new CodeCastExpression(typeof(Controllers), new CodePrimitiveExpression(midiEvent.Number)),
											new CodePrimitiveExpression(midiEvent.Value),
										});
			}

				// CHANNEL PRESSURE
			else if (ev is ChannelPressure)
			{
				ChannelPressure midiEvent = (ChannelPressure)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(ChannelPressure),
					new CodeExpression[]{
											delta, 
											new CodePrimitiveExpression(midiEvent.Channel),
											new CodePrimitiveExpression(midiEvent.Pressure)
				});
			}

				// PITCH WHEEL
			else if (ev is PitchWheel)
			{
				PitchWheel midiEvent = (PitchWheel)ev;
				newEvent = new CodeObjectCreateExpression(
					typeof(PitchWheel),
					new CodeExpression[]{
											delta, 
											new CodePrimitiveExpression(midiEvent.Channel),
											new CodePrimitiveExpression(midiEvent.UpperBits),
											new CodePrimitiveExpression(midiEvent.LowerBits)
										});
			}

			// Return the event
			return newEvent;
		}

		/// <summary>Creates a code expression that creates a byte array of data.</summary>
		/// <param name="data">The data to translate.</param>
		/// <returns>The code expression representing the array.</returns>
		private static CodeExpression CreateDataArray(byte [] data)
		{
			// Create an array of code expressions, each one a byte primitive
			CodeExpression [] initializers = new CodeExpression[data.Length];
			for(int i=0; i<data.Length; i++)
			{
				initializers[i] = new CodePrimitiveExpression(data[i]);
			}

			// Create the array of bytes used the created initialization list
			return new CodeArrayCreateExpression(typeof(byte), initializers);
		}
		#endregion
		#endregion
	}
}
